iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
Software Development

手刻 Redis:用 Go 從零打造高效能高併發的記憶體資料庫系列 第 18

Day 18:支援 Pipeline 批次命令處理與 TCP 讀取效能優化

  • 分享至 

  • xImage
  •  

如果要一次塞幾萬筆資料給 Redis,一問一答的網路來回延遲絕對會讓你等到睡著。

Redis 為了解這個問題提供了 Pipeline (管道)。今天就來看之前寫好的 TCP Server,能不能不用大改就吃下 Pipeline。


什麼是 Pipeline?

Pipeline 說穿了就是把多個命令先排在一起送出去,Server 再照順序解析、執行、回覆。重點不是單條命令變快,而是減少一問一答的網路等待。

沒有 Pipeline:
Client ---- SET a 1 ----> Server (執行)
Client <--- OK ---------- Server
Client ---- SET b 2 ----> Server (執行)
Client <--- OK ---------- Server

使用 Pipeline:
Client ---- SET a 1 \r\n SET b 2 ----> Server (執行 a, 執行 b)
Client <--- OK \r\n OK --------------- Server

用了 Pipeline,多個命令不用各自等一次 RTT,批量寫入時差距會很明顯。


我們的網路層如何天然支援 Pipeline?

在 Day 2 實作的 TCP 伺服器與 Day 13 整合的 handleConnection 中,我的程式碼是這樣寫的:

	// 使用 RESP Reader 解析客戶端傳來的資料
	reader := resp.NewReader(conn)
	for {
		val, err := reader.ReadValue()
		if err != nil {
			break
		}

		// 透過dispatcher 執行命令
		reply := s.dispatcher.Dispatch(clientCtx, val)

		// 如果有回覆,寫回 conn
		if reply.Type != 0 {
			_, err = conn.Write(reply.Marshal())
			if err != nil {
				break
			}
		}
	}

這個 Loop 看似是順序的,但正因為我底層使用了 bufio.Reader,它在進行系統呼叫 Read 時,會將 TCP 接收緩衝區中的大量資料一次性讀取到記憶體緩衝區中(預設為 4096 位元組)。

說實在,寫 Day 2 網路層的時候我還沒想那麼遠,只是直覺加上了 bufio.Reader。直到今天測試大量塞入資料,才發現原來 Go 的 bufio 早就幫我把 Pipeline 接收端需要的一次性讀取做掉了,有一種賺到的感覺。

當客戶端使用 Pipeline 一次性發送了 10 條命令時:

  1. net.Conn.Read 一次性將 10 條命令的 RESP 位元組讀入 bufio.Reader 緩衝區。
  2. 我們的 for 迴圈調用 reader.ReadValue(),它會從記憶體緩衝區中連續解析出 10 個 Value,少掉不少額外的系統呼叫(Syscall)。
  3. 每條命令執行完後,結果立即寫回 conn

所以雖然我們還沒有專門為 Pipeline 寫批次回覆邏輯,接收和解析這一段其實已經能吃下連續命令。


進一步的效能優化思考:緩衝寫入(bufio.Writer

目前,我們每次執行完命令,都直接呼叫 conn.Write 寫回客戶端。在高併發小命令的 Pipeline 場景下,這會觸發多次 write 系統呼叫,增加核心態(Kernel Mode)與用戶態(User Mode)切換的開銷。

如果後面要再優化,可以在連線寫入端加上 bufio.Writer

  • 將寫入的 RESP bytes 先暫存入緩衝區。
  • bufio.Reader 的內部緩衝區中沒有更多可讀資料時(即當前批次命令已全部執行完畢),主動呼叫 writer.Flush() 將所有回覆一次性推回給客戶端。

這類緩衝寫入也是 Redis 做網路 I/O 優化時會考慮的方向。之後如果要壓效能,可以把這塊補上。


跑起來看看

先把 Server 跑起來:

go run ./code/main.go

我們可以用 printfINFO 指令來看看伺服器目前的狀態。我們來實測一下:

# INFO
printf "*1\r\n\$4\r\nINFO\r\n" | nc localhost 6379

# 預期回覆:
# $...
# # Server
# redis_version:999.999.999
# os:Darwin
# arch_bits:64
#
# # Clients
# connected_clients:1
#
# # Memory
# used_memory:1048576
#
# # Persistence
# aof_enabled:0

總結

今天把 Pipeline 的接收流程看了一輪,也確認之前放的 bufio.Reader 剛好幫上忙。結果這邊為了算準記憶體使用量卡了老半天,還得拿 runtime.ReadMemStats 一個一個慢慢推算,Go 的垃圾回收機制有時候真的會讓這個數字跳來跳去的。

明天開始進到資料持久化,先寫 AOF 追加日誌。感覺那塊會有點麻煩。


上一篇
Day 17:發布與訂閱(Pub/Sub)廣播機制在 Go 中的設計與實作
下一篇
Day 19:Redis 持久化之 AOF (Append-Only File) 原理解析與基礎實作
系列文
手刻 Redis:用 Go 從零打造高效能高併發的記憶體資料庫25
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言